package com.kancy.slimmaker.core;

import com.kancy.slimmaker.log.Log;
import com.kancy.slimmaker.utils.DebugUtil;
import com.kancy.slimmaker.utils.FileUtils;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>
 * Main
 * <p>
 *
 * @author: kancy
 * @date: 2020/6/18 18:34
 **/

public class JarSlimMaker {

    private File jarFile;
    private List<String> packages = new ArrayList<>();
    private boolean isExcludePackage = false;

    private String workPath;
    private File workDirectory;
    private String classesPath;
    private File classesDirectory;
    private File classLoaderLogFile;
    private File tempLogFile;
    private String jarName;

    private boolean overwriteClassLoaderLog;
    private boolean deleteClassLoaderLog;


    public JarSlimMaker(File jarFile) {
        this(jarFile, Arrays.asList());
    }

    public JarSlimMaker(File jarFile, List<String> packages) {
        this.jarFile = jarFile;
        this.packages.addAll(packages);
        init();
    }

    private void init() {
        this.jarName = jarFile.getName();
        this.workDirectory = jarFile.getParentFile();
        this.workPath = safeFilePath(this.workDirectory.getAbsolutePath());
        this.classesPath = getClassesPath(this.workPath);
        this.classesDirectory = FileUtils.getFile(this.classesPath);
        this.classLoaderLogFile = FileUtils.getFile(this.workPath, "classloader.log");
        this.tempLogFile = FileUtils.getFile(FileUtils.getUserDirectoryPath(), "temp.log");
        setOverwriteClassLoaderLog(true);
        setDeleteClassLoaderLog(true);
    }

    private String safeFilePath(String absolutePath) {
        return absolutePath.replace("\\","/").replaceAll("/+","/");
    }

    public void slim() throws Exception {
        try {
           doSlim();
        } finally {
            // 删除临时数据
            if(this.deleteClassLoaderLog){
                deleteFile(this.classLoaderLogFile);
            }
            deleteFile(this.tempLogFile);
            deleteFile(this.classesDirectory);
        }
    }

    private void deleteFile(File file) {
        try {
            if (FileUtils.deleteQuietly(file)){
                FileUtils.forceDeleteOnExit(file);
            }
        } catch (IOException e) {
        }
    }

    private void doSlim() throws Exception {
        DebugUtil.startPoint();

        //  调用程序，用户操作，输出类加载日志
        ProcessBuilder processBuilder = null;
        Process process = null;
        if (!(!overwriteClassLoaderLog && classLoaderLogFile.exists() && classLoaderLogFile.isFile())){
            processBuilder = new ProcessBuilder("java","-verbose:class","-jar", jarName);
            processBuilder.directory(workDirectory);
            processBuilder.redirectOutput(classLoaderLogFile);
            process = processBuilder.start();
            process.waitFor();
        }

        List<String> classLoaderLogs = FileUtils.readLines(classLoaderLogFile);
        Log.info("1.完成类加载日志收集：{}" , classLoaderLogs.size());
        DebugUtil.printPoint("类加载日志收集");

        // 解压压缩包到临时文件夹
        processBuilder = new ProcessBuilder("jar","xf", jarFile.getAbsolutePath());
        processBuilder.directory(classesDirectory);
        processBuilder.redirectOutput(tempLogFile);
        process = processBuilder.start();
        process.waitFor();
        Log.info("2.解压jar包：{}" , jarName);
        DebugUtil.printPoint("解压jar包");

        // 分析日志：找到所有必须依赖的第三方包的class文件路径
        final Set<String> dependentClasses = new HashSet();
        long classCount = 0;
        for (String line : classLoaderLogs) {
            // 排除jre的类
            if (line.contains("\\lib\\rt.jar") || line.contains("__JVM_DefineClass__")){
                continue;
            }

            int startStrPosition = getStartStrPosition(line);
            int endStrPosition = getEndStrPosition(line);

            if (startStrPosition > -1 && endStrPosition > -1){
                String className = line.substring(startStrPosition, endStrPosition);
                String classFilePath = getClassFilePath(classesPath, className);
                if (packages.isEmpty() || isExcludePackage){
                    classCount++;
                    dependentClasses.add(classFilePath);
                    continue;
                }

                for (String packagePrefix : packages) {
                    if (className.startsWith(packagePrefix)){
                        classCount++;
                        dependentClasses.add(classFilePath);
                        continue;
                    }
                }
            }
        }
        Log.info("3.找到必须加载的类数量：{}" , classCount);
        DebugUtil.printPoint("解析类加载日志");

        // 删除没有用到的文件
        // 便利临时目录，移除所有不匹配简化包的且不在sets中的文件
        AtomicInteger deleteCount = new AtomicInteger();
        deleteFiles(classesPath, packages, dependentClasses, new File(classesPath), deleteCount);
        Log.info("4.已删除所有无需加载的类文件数量：{}" , deleteCount.intValue());
        DebugUtil.printPoint("删除无需加载的类");

        // 重新打包
        String newJarName = newJarName(jarName);
        processBuilder = new ProcessBuilder("jar","cfM", newJarName, "*");
        processBuilder.directory(new File(classesPath));
        processBuilder.redirectOutput(tempLogFile);
        process = processBuilder.start();
        process.waitFor();

        Log.info("5.重新打包完成：{}" , newJarName);
        DebugUtil.printPoint("重新打包");

        // 移动简化的jar包到工作目录
        File deleteFile = FileUtils.getFile(workPath, newJarName(jarName));
        boolean deleteQuietly = true;
        if (deleteFile.exists()){
            deleteQuietly = FileUtils.deleteQuietly(deleteFile);
        }

        if (deleteQuietly){
            FileUtils.moveFileToDirectory(FileUtils.getFile(classesPath, newJarName(jarName)), workDirectory, true);
            Log.info("6.移动文件到工作目录：{}" , newJarName);
        }else {
            Log.info("6.移动文件到工作目录失败，请先删除文件：{}" , deleteFile.getAbsolutePath());
        }
        DebugUtil.printPoint("文件移动");

        Log.info("7.完成");
        DebugUtil.endPoint("完成");
    }

    private int getEndStrPosition(String line) {
        // JDK8
        int index = line.indexOf(" from ");
        if (index > -1){
            return index;
        }

        // JDK11
        index = line.indexOf(" source:");
        if (index > -1){
            return index;
        }
        return -1;
    }

    private int getStartStrPosition(String line) {
        // JDK8
        int index = line.indexOf("[Loaded ");
        if (index > -1){
            index += 8;
            return index;
        }

        // JDK11
        index = line.indexOf("[class,load] ");
        if (index > -1){
            index += 13;
            return index;
        }
        return -1;
    }

    private void deleteFiles(final String classesTempPath, final List<String> packagePrefixs, final Set<String> sets, File root, AtomicInteger deleteCount) {
        root.listFiles(pathname -> {
            boolean result = false;

            if(pathname.isDirectory()){
                deleteFiles(classesTempPath, packagePrefixs, sets, pathname,deleteCount);
                return result;
            }

            if (isExcludePackage || packagePrefixs.isEmpty()){
                String filePath = pathname.getAbsolutePath().replace("\\","/");
                // 不删除被排除的包
                boolean canDelete = true;
                if (!packagePrefixs.isEmpty()){
                    for (String packagePrefix : packagePrefixs) {
                        // 如果路径匹配前缀则退出
                        String classFilePathPrefix = getClassFilePathPrefix(classesTempPath, packagePrefix);
                        if (filePath.startsWith(classFilePathPrefix)){
                            canDelete = false;
                        }
                    }
                }

                // 需要删除吗？
                if (canDelete && !sets.contains(filePath) && filePath.endsWith(".class")){
                    FileUtils.deleteQuietly(pathname);
                    deleteCount.incrementAndGet();
                    Log.debug("Delete : " + filePath);
                }
            }

            if (!isExcludePackage){
                for (String packagePrefix : packagePrefixs) {
                    String path = String.format("%s/%s", classesTempPath, className2Path(packagePrefix));
                    // 如果路径匹配前缀则退出
                    String filePath = pathname.getAbsolutePath().replace("\\","/");
                    if (filePath.startsWith(path)){
                        // 需要删除吗？
                        if (!sets.contains(filePath) && filePath.endsWith(".class")){
                            FileUtils.deleteQuietly(pathname);
                            deleteCount.incrementAndGet();
                            Log.debug("Delete : " + filePath);
                        }
                    }
                }
            }

            return result;
        });
    }


    private static String newJarName(String jarName) {
        return jarName.replace(".jar","-slim.jar")
                .replace(".JAR","-slim.jar");
    }

    private static String getClassesPath(String workPath) {
        String filePath = String.format("%s/classes", workPath).replaceAll("/+","/");
        File file = FileUtils.getFile(filePath);
        if (!file.exists() || !file.isDirectory()){
            file.mkdirs();
        }
        return filePath;
    }

    private static String getClassFilePath(String classesBasePath, String className){
        return String.format("%s/%s.class", classesBasePath, className2Path(className))
                .replaceAll("/+", "/");
    }
    private static String getClassFilePathPrefix(String classesBasePath, String packageName){
        return String.format("%s/%s/", classesBasePath, className2Path(packageName))
                .replaceAll("/+", "/");
    }

    private static String className2Path(String packageName){
        return packageName.replace(".","/");
    }

    public void setOverwriteClassLoaderLog(boolean overwriteClassLoaderLog) {
        this.overwriteClassLoaderLog = overwriteClassLoaderLog;
    }

    public void setDeleteClassLoaderLog(boolean deleteClassLoaderLog) {
        this.deleteClassLoaderLog = deleteClassLoaderLog;
    }

    public void setExcludePackage(boolean excludePackage) {
        isExcludePackage = excludePackage;
    }
}
